D:\a\csshw\csshw\xtask\src\coverage.rs
Line | Count | Source |
1 | | //! Local coverage report generation. |
2 | | //! |
3 | | //! The nightly toolchain is required because `#[coverage(off)]` — used to |
4 | | //! exclude untestable code such as Windows API wrappers and production I/O |
5 | | //! implementations from coverage — relies on the `coverage_attribute` feature, |
6 | | //! which is only available on nightly Rust. Without it the `cfg(coverage_nightly)` |
7 | | //! guards would not activate, causing those impls to be counted as missed lines |
8 | | //! and distorting the report. |
9 | | //! |
10 | | //! The pinned toolchain version is read from |
11 | | //! `.config/coverage/nightly-toolchain.version` and the filename exclusion |
12 | | //! regex is read from `.config/coverage/ignore-filename.regex`; both files |
13 | | //! are shared with the CI workflow to keep the environments in sync. |
14 | | //! |
15 | | //! [`run_coverage`] orchestrates the full workflow: toolchain check, |
16 | | //! instrumented test run, and report generation. |
17 | | |
18 | | use anyhow::{bail, Context, Result}; |
19 | | |
20 | | /// All side-effecting operations required by this module. |
21 | | /// |
22 | | /// Implement with mocks in tests to achieve zero filesystem, process, |
23 | | /// and toolchain side-effects. |
24 | | pub trait CoverageSystem { |
25 | | /// Read the contents of `.config/coverage/nightly-toolchain.version` and |
26 | | /// return the trimmed toolchain identifier (e.g. `nightly-2026-04-20`). |
27 | | /// |
28 | | /// # Errors |
29 | | /// |
30 | | /// Returns an error if the file cannot be read. |
31 | | fn read_nightly_version_file(&self) -> Result<String>; |
32 | | |
33 | | /// Run `rustup toolchain list` and return its stdout. |
34 | | /// |
35 | | /// # Errors |
36 | | /// |
37 | | /// Returns an error if the process cannot be started. |
38 | | fn list_installed_toolchains(&self) -> Result<String>; |
39 | | |
40 | | /// Run `rustup toolchain install <toolchain> --component llvm-tools`. |
41 | | /// |
42 | | /// # Errors |
43 | | /// |
44 | | /// Returns an error if the install fails. |
45 | | fn install_toolchain(&self, toolchain: &str) -> Result<()>; |
46 | | |
47 | | /// Run a `cargo +<toolchain> llvm-cov` subcommand with the given arguments. |
48 | | /// |
49 | | /// # Errors |
50 | | /// |
51 | | /// Returns an error if the command fails. |
52 | | fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()>; |
53 | | |
54 | | /// Read the contents of `.config/coverage/ignore-filename.regex` and |
55 | | /// return the trimmed filename regex pattern passed to |
56 | | /// `--ignore-filename-regex`. |
57 | | /// |
58 | | /// # Errors |
59 | | /// |
60 | | /// Returns an error if the file cannot be read. |
61 | | fn read_ignore_regex_file(&self) -> Result<String>; |
62 | | |
63 | | /// Print an informational message to stdout. |
64 | | fn print_info(&self, message: &str); |
65 | | } |
66 | | |
67 | | /// Production implementation of [`CoverageSystem`]. |
68 | | pub struct RealSystem; |
69 | | |
70 | | #[cfg_attr(coverage_nightly, coverage(off))] |
71 | | impl CoverageSystem for RealSystem { |
72 | | fn read_nightly_version_file(&self) -> Result<String> { |
73 | | std::fs::read_to_string(".config/coverage/nightly-toolchain.version") |
74 | | .context("failed to read .config/coverage/nightly-toolchain.version") |
75 | | .map(|s| s.trim().to_owned()) |
76 | | } |
77 | | |
78 | | fn list_installed_toolchains(&self) -> Result<String> { |
79 | | let output = std::process::Command::new("rustup") |
80 | | .args(["toolchain", "list"]) |
81 | | .output() |
82 | | .context("failed to run `rustup toolchain list`")?; |
83 | | Ok(String::from_utf8_lossy(&output.stdout).into_owned()) |
84 | | } |
85 | | |
86 | | fn install_toolchain(&self, toolchain: &str) -> Result<()> { |
87 | | let status = std::process::Command::new("rustup") |
88 | | .args([ |
89 | | "toolchain", |
90 | | "install", |
91 | | toolchain, |
92 | | "--component", |
93 | | "llvm-tools", |
94 | | ]) |
95 | | .status() |
96 | | .context("failed to run `rustup toolchain install`")?; |
97 | | if !status.success() { |
98 | | bail!("`rustup toolchain install {toolchain}` failed with status {status}"); |
99 | | } |
100 | | Ok(()) |
101 | | } |
102 | | |
103 | | fn run_cargo_llvm_cov(&self, toolchain: &str, args: &[String]) -> Result<()> { |
104 | | let toolchain_arg = format!("+{toolchain}"); |
105 | | let status = std::process::Command::new("cargo") |
106 | | .arg(&toolchain_arg) |
107 | | .arg("llvm-cov") |
108 | | .args(args) |
109 | | .status() |
110 | | .with_context(|| { |
111 | | format!( |
112 | | "failed to run `cargo {toolchain_arg} llvm-cov {}`", |
113 | | args.join(" ") |
114 | | ) |
115 | | })?; |
116 | | if !status.success() { |
117 | | bail!( |
118 | | "`cargo {toolchain_arg} llvm-cov {}` failed with status {status}", |
119 | | args.join(" ") |
120 | | ); |
121 | | } |
122 | | Ok(()) |
123 | | } |
124 | | |
125 | | fn read_ignore_regex_file(&self) -> Result<String> { |
126 | | std::fs::read_to_string(".config/coverage/ignore-filename.regex") |
127 | | .context("failed to read .config/coverage/ignore-filename.regex") |
128 | | .map(|s| s.trim().to_owned()) |
129 | | } |
130 | | |
131 | | fn print_info(&self, message: &str) { |
132 | | println!("INFO - {message}"); |
133 | | } |
134 | | } |
135 | | |
136 | | /// Convert a slice of string literals to a `Vec<String>`. |
137 | 13 | fn args(values: &[&str]) -> Vec<String> { |
138 | 59 | values13 .iter13 ().map13 (|s| (*s).to_owned()).collect13 () |
139 | 13 | } |
140 | | |
141 | | /// Generate coverage reports using the pinned nightly toolchain. |
142 | | /// |
143 | | /// Reads the toolchain identifier from |
144 | | /// `.config/coverage/nightly-toolchain.version`, ensures it is installed, |
145 | | /// cleans stale coverage data, runs the test suite with instrumentation, |
146 | | /// and produces Cobertura XML and HTML reports. |
147 | | /// |
148 | | /// # Arguments |
149 | | /// |
150 | | /// * `system` - Injected I/O provider. |
151 | | /// |
152 | | /// # Errors |
153 | | /// |
154 | | /// Returns an error if any step fails (missing version file, toolchain |
155 | | /// install failure, test failure, or report generation failure). |
156 | 6 | pub fn run_coverage<S: CoverageSystem>(system: &S) -> Result<()> { |
157 | 6 | let toolchain5 = system.read_nightly_version_file()?1 ; |
158 | 5 | system.print_info(&format!("Using nightly toolchain: {toolchain}")); |
159 | 5 | let ignore_regex = system.read_ignore_regex_file()?0 ; |
160 | | |
161 | | // Ensure toolchain is installed. |
162 | 5 | let installed = system.list_installed_toolchains()?0 ; |
163 | 5 | if installed.lines().any(|line| line.starts_with(&toolchain)) { |
164 | 3 | system.print_info("Toolchain already installed"); |
165 | 3 | } else { |
166 | 2 | system.print_info(&format!("Installing toolchain: {toolchain}")); |
167 | 2 | system.install_toolchain(&toolchain)?1 ; |
168 | | } |
169 | | |
170 | | // Clean previous coverage data. |
171 | 4 | system.print_info("Cleaning previous coverage data"); |
172 | 4 | system.run_cargo_llvm_cov(&toolchain, &args(&["clean", "--workspace"]))?1 ; |
173 | | |
174 | | // Run tests with coverage instrumentation. |
175 | 3 | system.print_info("Running tests with coverage"); |
176 | 3 | system.run_cargo_llvm_cov( |
177 | 3 | &toolchain, |
178 | 3 | &args(&[ |
179 | 3 | "--all-features", |
180 | 3 | "--workspace", |
181 | 3 | "--no-report", |
182 | 3 | "--", |
183 | 3 | "--no-capture", |
184 | 3 | ]), |
185 | 0 | )?; |
186 | | |
187 | | // Generate Cobertura XML report. |
188 | 3 | system.print_info("Generating Cobertura XML report"); |
189 | 3 | system.run_cargo_llvm_cov( |
190 | 3 | &toolchain, |
191 | 3 | &args(&[ |
192 | 3 | "report", |
193 | 3 | "--cobertura", |
194 | 3 | "--output-path", |
195 | 3 | "coverage.xml", |
196 | 3 | "--ignore-filename-regex", |
197 | 3 | &ignore_regex, |
198 | 3 | ]), |
199 | 0 | )?; |
200 | | |
201 | | // Generate HTML report. |
202 | 3 | system.print_info("Generating HTML report"); |
203 | 3 | system.run_cargo_llvm_cov( |
204 | 3 | &toolchain, |
205 | 3 | &args(&[ |
206 | 3 | "report", |
207 | 3 | "--html", |
208 | 3 | "--output-dir", |
209 | 3 | "coverage_html", |
210 | 3 | "--ignore-filename-regex", |
211 | 3 | &ignore_regex, |
212 | 3 | ]), |
213 | 0 | )?; |
214 | | |
215 | 3 | system.print_info("Coverage reports generated:"); |
216 | 3 | system.print_info(" XML: coverage.xml"); |
217 | 3 | system.print_info(" HTML: coverage_html/index.html"); |
218 | 3 | Ok(()) |
219 | 6 | } |
220 | | |
221 | | #[cfg(test)] |
222 | | #[path = "tests/test_coverage.rs"] |
223 | | mod tests; |